﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace AgileDesign.Utilities
{
    public static class ConvertExtensions
    {
        private const BindingFlags BindingFlagsToUse = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

        public static T ConvertTo<T>(this object value)
        {
            object result = value.ConvertTo(typeof(T));
            if (result == null)
            {
                return default(T);
            }
            return (T)result;
        }

        public static object ConvertTo
            (
            this object value,
            Type targetType
            )
        {
            Contract.Requires<ArgumentNullException>(targetType != null);
            if (value == null)
            {
                return null;
            }
            object result;
            try
            {
                if (! targetType.IsGenericType)
                {
                    result = Convert(value, targetType);
                }
                else
                {
                    var genericArgs = targetType.GetGenericArguments();
                    if (genericArgs.Length != 1)
                    {
                        throw new NotSupportedException
                            ("Generics with multiple arguments are not supported");
                    }
                    result = Convert(value, genericArgs[0]);
                }
            }
            catch (Exception)
            {
                return CloneConvertTo(value, targetType);
            }
            if (result == null)
            {
                CloneConvertTo(value, targetType);
            }
            return result;
        }

        private static object CloneConvertTo(object value, Type targetType)
        {
            return targetType.IsInstanceOfType(value)
                ? value
                : value.CloneConvert(targetType);
        }

        private static object Convert(object value, Type targetType)
        {
            var converter = TypeDescriptor.GetConverter(targetType);
            if (converter.IsValid(value))
            {
                return converter.ConvertFrom(value);
            }
            return System.Convert.ChangeType(value, targetType);
        }


        public static T CloneConvert<T>(this object value)
        {
            return (T)CloneConvert(value, typeof (T));
        }

        public static object CloneConvert(this object value, Type targetType)
        {
            try
            {
                var result = Activator.CreateInstance(targetType);
                if (result == null)
                {
                    if (IsConversionExceptionEnabled)
                    {
                        throw CreateFailedToConvertException(value, targetType, null);
                    }
                    LogConversionError(value, targetType);
                    return null;
                }
                result = CopyTo(value, result);
                if (Equals(result, Default(targetType)))
                {
                    if (IsConversionExceptionEnabled)
                    {
                        throw CreateFailedToConvertException(value, targetType, null);
                    }
                    LogConversionError(value, targetType);
                    return null;
                }
                return result;
            }
            catch (Exception ex)
            {
                throw CreateFailedToConvertException(value, targetType, ex);
            }
        }

        private static bool _isConversionExceptionEnabled = true;
        public static bool IsConversionExceptionEnabled
        {
            get { return _isConversionExceptionEnabled; }
            set { _isConversionExceptionEnabled = value; }
        }

        private static void LogConversionError(object value, Type targetType)
        {
            Logger.TraceInformation("Error converting value '{0}' into type '{1}'"
                , value, targetType.Name);
        }

        static object Default(Type targetType)
        {
            return (targetType.IsValueType )
                ? Activator.CreateInstance(targetType) 
                : null;
        }

        /// <summary>
        /// Makes a shallow copy of source properties into target properties.
        /// For deep copy use Value Injecter: <see cref="https://valueinjecter.codeplex.com/"/> 
        /// </summary>
        /// <returns>A object with a shallow copy of source properties</returns>
        public static object CopyTo(this object source, object target)
        {
            var targetType = target.GetType();
            var targetProps = targetType.GetProperties(BindingFlagsToUse);
            var targetFields = targetType.GetFields(BindingFlagsToUse);
            var sourceType = source.GetType();
            foreach (var targetPropertyInfo in targetProps.Where(propertyInfo => propertyInfo.CanWrite))
            {
                var sourceProperty = sourceType.GetProperty(targetPropertyInfo.Name, BindingFlagsToUse);
                if (sourceProperty == null 
                    ||  ! sourceProperty.CanRead)
                {
                    continue;
                }
                targetPropertyInfo.SetValue
                    (
                        target
                        , (targetPropertyInfo.PropertyType == sourceProperty.PropertyType) 
                            ? sourceProperty.GetValue(source, null)
                            : System.Convert.ChangeType(sourceProperty.GetValue(source, null), targetPropertyInfo.PropertyType)
                        , null
                    );
            }
            foreach (var targetFieldInfo in targetFields)
            {
                var sourceFieldInfo = sourceType.GetField(targetFieldInfo.Name, BindingFlagsToUse);
                if (sourceFieldInfo == null)
                {
                    continue;
                }
                targetFieldInfo.SetValue
                    (
                        target,
                        (targetFieldInfo.FieldType == sourceFieldInfo.FieldType)
                            ? sourceFieldInfo.GetValue(source)
                            : System.Convert.ChangeType(sourceFieldInfo.GetValue(source), targetFieldInfo.FieldType)
                    );
            }
            return target;
        }

        /// <summary>
        /// Makes a shallow copy of source property values into null target properties
        /// For deep copy use Value Injecter: <see cref="https://valueinjecter.codeplex.com/"/> 
        /// </summary>
        public static object MergeTo(this object source, object target)
        {
            var targetType = target.GetType();
            var targetProps = targetType.GetProperties(BindingFlagsToUse);
            var targetFields = targetType.GetFields(BindingFlagsToUse);
            var sourceType = source.GetType();
            foreach (var targetPropertyInfo in targetProps
                .Where(propertyInfo => propertyInfo.CanWrite
                    && propertyInfo.CanRead
                    && propertyInfo.GetValue(target, null) == null))
            {
                var sourceProperty = sourceType.GetProperty(targetPropertyInfo.Name, BindingFlagsToUse);
                if (sourceProperty == null 
                    || ! sourceProperty.CanRead)
                {
                    continue;
                }
                var sourcePropertyValue = sourceProperty.GetValue(source, null);
                targetPropertyInfo.SetValue(target, sourcePropertyValue,
                                      null);
            }
            foreach (var fieldInfo in targetFields
                .Where(fieldInfo => fieldInfo.GetValue(target) == null))
            {
                var sourceFieldInfo = sourceType.GetField(fieldInfo.Name, BindingFlagsToUse);
                if (sourceFieldInfo == null)
                {
                    continue;
                }
                fieldInfo.SetValue(target, sourceFieldInfo.GetValue(source));
            }
            return target;
        }

        private static InvalidCastException CreateFailedToConvertException(object value, Type targetType, Exception originalException)
        {
            return new InvalidCastException(
                string.Format("Failed to convert type '{0}' into type '{1}'", value.GetType().FullName,
                              targetType.FullName), originalException);
        }
    }
}